From: Nate Berkopec Date: Tue, 26 May 2026 23:33:21 +0000 (+0900) Subject: [PATCH] 7.2.1 backport (#3947) X-Git-Tag: archive/raspbian/5.6.5-3+rpi1+deb12u2^2~2 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=faec6e7dff69dd955fccf419ba3a84f732f92b27;p=puma.git [PATCH] 7.2.1 backport (#3947) * Limit and anchor PROXY protocol parsing (cherry picked from commit f1f37d5822aa8ffe23e7d0d16e9a1f3b0eaf9bfd) * Parse PROXY protocol only once per connection (cherry picked from commit 8310c17cd43784723ed41a94d2a747da256306de) Gbp-Pq: Name CVE-2026-47736_37.patch --- diff --git a/lib/puma/client.rb b/lib/puma/client.rb index b5a1569..55d7982 100644 --- a/lib/puma/client.rb +++ b/lib/puma/client.rb @@ -148,7 +148,7 @@ module Puma def reset(fast_check=true) @parser.reset @read_header = true - @read_proxy = !!@expect_proxy_proto + @read_proxy = !!@expect_proxy_proto && @requests_served.zero? @env = @proto_env.dup @body = nil @tempfile = nil @@ -196,20 +196,36 @@ module Puma def try_to_parse_proxy_protocol if @read_proxy if @expect_proxy_proto == :v1 - if @buffer.include? "\r\n" - if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer) - if md[1] - @peerip = md[1].split(" ")[0] + crlf_index = @buffer.index "\r\n" + + unless crlf_index + if "PROXY ".start_with? @buffer + return false + elsif @buffer.start_with? "PROXY " + if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH + raise ConnectionError, "PROXY protocol v1 line is too long" end - @buffer = md.post_match + return false end - # if the buffer has a \r\n but doesn't have a PROXY protocol - # request, this is just HTTP from a non-PROXY client; move on + @read_proxy = false - return @buffer.size > 0 - else - return false + return true + end + + if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH + raise ConnectionError, "PROXY protocol v1 line is too long" + end + + if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer) + if md[1] + @peerip = md[1].split(" ")[0] + end + @buffer = md.post_match end + # if the buffer has a \r\n but doesn't have a PROXY protocol + # request, this is just HTTP from a non-PROXY client; move on + @read_proxy = false + return @buffer.size > 0 end end true diff --git a/lib/puma/const.rb b/lib/puma/const.rb index 12b8bfd..048ed61 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -255,6 +255,7 @@ module Puma # Banned keys of response header BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze - PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze + PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze + PROXY_PROTOCOL_V1_MAX_LENGTH = 107 end end diff --git a/test/test_puma_server.rb b/test/test_puma_server.rb index 81bf23e..52a45c2 100644 --- a/test/test_puma_server.rb +++ b/test/test_puma_server.rb @@ -1065,6 +1065,75 @@ EOF assert_equal 'fd00::1', remote_addr end + def test_proxy_protocol_rejects_line_without_crlf_at_max_length + server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do + [200, {}, ["Hello"]] + end + + socket = new_connection + socket << "PROXY #{'A' * (Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH - "PROXY ".bytesize)}" + + assert_equal "", socket.read + end + + def test_proxy_protocol_allows_non_proxy_requests_over_max_length + server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do + [200, {}, ["Hello"]] + end + + response = send_http_and_read( "GET /#{'a' * Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH} HTTP/1.0\r\n\r\n") + + assert_match %r{\AHTTP/1\.[01] 200 OK}, response + assert_match %r{Hello\z}, response + end + + def test_proxy_protocol_ignores_embedded_proxy_line_in_http_body + server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env| + body = env['rack.input'].read + [200, {'Content-Length' => body.bytesize.to_s}, [body]] + end + + body = "prefix\nPROXY TCP4 1.1.1.1 2.2.2.2 3 4\r\nsuffix" + response = send_http_and_read ("POST / HTTP/1.1\r\nHost: test.com\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}") + + assert_match %r{\AHTTP/1\.[01] 200 OK}, response + assert_match %r{#{Regexp.escape(body)}\z}, response + end + + def test_proxy_protocol_only_parses_first_request_on_connection + server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env| + [200, {}, [env["REMOTE_ADDR"]]] + end + + socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4") + + assert_equal ["HTTP/1.1 200 OK", "Content-Length: 7"], header(socket) + assert_equal "1.2.3.4", socket.read(7) + + sleep 1 + + socket << "PROXY TCP4 127.0.0.1 127.0.0.1 10000 80\r\nGET /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n" + + assert_match %r{\AHTTP/1\.[01] 400 Bad Request}, socket.read + end + + def test_proxy_protocol_allows_proxy_http_method_on_keep_alive + server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1, supported_http_methods: :any) do |env| + [200, {}, [env["REMOTE_ADDR"]]] + end + + socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4") + + assert_equal ["HTTP/1.1 200 OK", "Content-Length: 7"], header(socket) + assert_equal "1.2.3.4", socket.read(7) + + socket << "PROXY /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n" + + response = socket.read + assert_match %r{\AHTTP/1\.1 200 OK\r\n}, response + assert_equal "1.2.3.4", response.split("\r\n").last + end + # To comply with the Rack spec, we have to split header field values # containing newlines into multiple headers. def assert_does_not_allow_http_injection(app, opts = {})